<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <table border style="width: 100%; border-collapse: collapse;">
    <thead id="table-header"></thead>
    <tbody id="table-body"></tbody>
  </table>

  <script>
    const data = [
      {name: '张三', age: 20, sex: '男'},
      {name: '张三', age: 20, sex: '女'},
      {name: '李四', age: 30, sex: '男'},
      {name: '王五', age: 15, sex: '男'},
      {name: '王五', age: 20, sex: '男'},
      {name: '张三', age: 18, sex: '男'},
    ]
    const columns = [
      { id: '1', title: "姓名", dataIndex: "name" },
      { id: '2', title: "年龄", dataIndex: "age" },
      { id: '3', title: "性别", dataIndex: "sex" },
    ];

    /**
     * 获取单元格合并信息
     * @param {array} columns 字段列表 
     * @param {array} data 数据
     * @returns 
     */
    const getCellMerge = function (columns, data) {
      const cellMap = {};
      const createCellMergeItem = function (start, end, rowspan, value) {
        return { startIndex: start, endIndex: end, rowspan, value: value };
      };
      const diffParent = function (rowIndex, prevRowIndex, columnIndex) {
        let rowRes = [];
        let prevRowRes = [];
        // 依次从查询字段对比到第一个字段，是否一样；
        while (columnIndex >= 0) {
          const col = columns[columnIndex];
          rowRes.unshift(data[rowIndex][col.dataIndex]);
          prevRowRes.unshift(data[prevRowIndex][col.dataIndex]);
          columnIndex--;
        }
        return rowRes.join("") === prevRowRes.join("");
      };
      const setMergeInfo = function (dataIndex, prev, rowspan) {
        // 找当跟上一个一个组的成员，设置偏移数量
        cellMap[dataIndex] = cellMap[dataIndex].map((row, idx) => {
          if (row.startIndex === prev.startIndex && row.value === prev.value) {
            row.rowspanIndex = idx - row.startIndex;
            row.endIndex = prev.startIndex + rowspan - 1;
            row.rowspan = rowspan;
          }
          return row;
        });
      };
      for (let j = 0; j < columns.length; j++) {
        const { dataIndex } = columns[j];
        let rowspan = 1;
        let startIndex = 0;
        for (let i = 0; i < data.length; i++) {
          const rowValue = data[i][dataIndex];
          if (!cellMap[dataIndex]) {
            cellMap[dataIndex] = [];
          }
          const diffCur = i > 0 && rowValue !== undefined && rowValue === data[i - 1][dataIndex];
          const diffParents = i > 0 && (j === 0 ? true : diffParent(i, i - 1, j));

          /**
           * 1、对比当前字段与上一条记录的同一字段是否一样；
           * 2、对比当前的上n个字段与上一条的记录的上n个字段是否一样（简单的说就是判断是否在一个分组下）
           * 两个条件都满足情况下合并；
           */
          if (diffCur && diffParents) {
            rowspan++;
            // 是否为最后一条数据
          } else {
            // 如果与上一个值不一样
            setMergeInfo(dataIndex, cellMap[dataIndex][i - 1], rowspan);
            startIndex = i;
            rowspan = 1;
          }
          cellMap[dataIndex].push(createCellMergeItem(startIndex, i, rowspan, rowValue));
          // 是否为最后一条数据
          if (i === data.length - 1) {
            setMergeInfo(dataIndex, cellMap[dataIndex][i], rowspan);
          }
        }
      }
      return cellMap;
    };

    
    // 渲染表头
    const renderTableHeader = function (columns) {
      const headerEl = document.getElementById('table-header');
      const headerHtml = '<tr>' + columns.map(itm => `<th>${itm.title}</th>`).join('') + '</tr>';
      headerEl.innerHTML = headerHtml;
    }

    // 渲染body
    const renderTableBody = function (columns, data) {
      const cellMerge = getCellMerge(columns, data);
      console.log("单元格合并信息：", cellMerge)
      const bodyEl = document.getElementById('table-body');
      const bodyHtml = data.map((row, rowIdx) => {
        let tds = columns.map(col => {
          const { dataIndex } = col;
          const mergeInfo = cellMerge[dataIndex][rowIdx]
          // 存在合并，并且是合并的第一个；
          if (mergeInfo.rowspan > 1 && mergeInfo.rowspanIndex === 0) {
            return `<td data-index="${col.dataIndex}-${rowIdx}" rowspan="${mergeInfo.rowspan}">${typeof row[col.dataIndex] === 'undefined' ? '' : row[col.dataIndex]}</td>`
          }
          // 存在合并，但是不是合并的头部第一个，所以需要删除
          if (mergeInfo.rowspan === 1) {
            return `<td data-index="${col.dataIndex}-${rowIdx}">${typeof row[col.dataIndex] === 'undefined' ? '' : row[col.dataIndex]}</td>`;
          }
          return ''
        })
        let rowStr = `<tr>\n${tds.join('\n')}\n</tr>`;
        return rowStr
      }).join('\n')
      bodyEl.innerHTML = bodyHtml;
    }

    renderTableHeader(columns);
    renderTableBody(columns, data)

  </script>
</body>
</html>